package org.shj.netty.chartper10;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;

public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
	
	private final static AsciiString CONTENT_TYPE = HttpHeaderNames.CONTENT_TYPE;
	private final static AsciiString CONNECTION = HttpHeaderNames.CONNECTION;
	private final static AsciiString LOCATION = HttpHeaderNames.LOCATION;
	
	private final String url;
	
	public HttpFileServerHandler(String url){
		this.url = url;
	}
	
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception{
		if(!req.decoderResult().isSuccess()){
			sendError(ctx, HttpResponseStatus.BAD_REQUEST);
			return;
		}
		
		if(req.method() != HttpMethod.GET){
			sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
			return;
		}
		
		final String uri = req.uri();
		final String path = sanitizedUri(uri);
		if(path == null){
			sendError(ctx, HttpResponseStatus.FORBIDDEN);
			return;
		}
		
		File file = new File(path);
		if(file.isHidden() || !file.exists()){
			sendError(ctx, HttpResponseStatus.NOT_FOUND);
			return;
		}
		
		if(file.isDirectory()){
			if(uri.endsWith("/")){
				sendListing(ctx, file);
			}else{
				sendRedirect(ctx, uri + "/");
				return;
			}
		}
		
		if(!file.isFile()){
			sendError(ctx, HttpResponseStatus.FORBIDDEN);
			return;
		}
		
		RandomAccessFile randomAccessFile = null;
		try{
			randomAccessFile = new RandomAccessFile(file, "r");
		}catch(FileNotFoundException fnfe){
			sendError(ctx, HttpResponseStatus.NOT_FOUND);
			return;
		}
		
		long fileLength = randomAccessFile.length();
		HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		HttpUtil.setContentLength(res, fileLength);
		setContentTypeHeader(res, file);
		if(HttpUtil.isKeepAlive(req)){
			res.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
		}
		
		ctx.write(res);
		ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
		sendFileFuture.addListener(new ChannelProgressiveFutureListener(){

			@Override
			public void operationProgressed(ChannelProgressiveFuture future,
					long progress, long total) throws Exception {
				if(total < 0){
					System.err.println("Transfer progress: " + progress);
				}else{
					System.err.println("Transfer progress: " + progress  + "/" + total);
				}
				
			}

			@Override
			public void operationComplete(ChannelProgressiveFuture future)
					throws Exception {
				System.out.println("Transfer complete.");
			}
			
		});
		
		ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
		if(!HttpUtil.isKeepAlive(req)){
			lastContentFuture.addListener(ChannelFutureListener.CLOSE);
		}
	}
	
	private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
	
	private String sanitizedUri(String uri){
		try{
			uri = URLDecoder.decode(uri,"UTF-8");
			
		}catch(UnsupportedEncodingException e){
			try{
				uri = URLDecoder.decode(uri, "ISO-8859-1");
				
			}catch(UnsupportedEncodingException el){
				throw new Error();
			}
		}
		
		if (!uri.startsWith(url)){
			return null;
		}
		
		if (!uri.startsWith("/")){
			return null;
		}
		
		uri = uri.replace('/', File.separatorChar);
		if(uri.contains(File.separator + '.')
			|| uri.contains('.'+ File.separator) || uri.startsWith(".")
			|| uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()){
			return null;
		}
		return System.getProperty("user.dir") + File.separator + uri;
	}
	
	private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
	
	private static void sendListing(ChannelHandlerContext ctx, File dir){
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		response.headers().set(CONTENT_TYPE, "text/html;charset=UTF-8");
		StringBuilder buf = new StringBuilder();
		String dirPath = dir.getPath();
		buf.append("<!DOCTYPE html>\r\n");
		buf.append("<html><head><title>");
		buf.append(dirPath)
		   .append(" 目录： ")
		   .append("</title></head><body>\r\n<h3>")
		   .append(dirPath).append(" 目录： </h3>\r\n")
		   .append("<ul>")
		   .append("<li>链接： <a href=\"../\">..</a></li>\r\n");
		
		for(File f : dir.listFiles()){
			if(f.isHidden() || !f.canRead()){
				continue;
			}
			String name = f.getName();
			if(!ALLOWED_FILE_NAME.matcher(name).matches()){
				continue;
			}
			buf.append("<li>链接： <a href=\"").append(name).append("\">").append(name).append("</a></li>\r\n");
			
		}
		buf.append("</ul></body></html>\r\n");
		ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
		response.content().writeBytes(buffer);
		buffer.release();
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
		
	}
	
	private static void sendRedirect(ChannelHandlerContext ctx, String newUri){
		FullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
		res.headers().set(LOCATION, newUri);
		ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
	}
	
	private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
		FullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure:" + status.toString() + "\r\n", CharsetUtil.UTF_8));
		res.headers().set(CONTENT_TYPE, "text/plain;charset=UTF-8");
		ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
	}
	
	private static void setContentTypeHeader(HttpResponse res, File file){
		MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
		res.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
	}
	
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
		cause.printStackTrace();
		if(ctx.channel().isActive()){
			sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
		}
	}

	
}
